package com.magenta.maxoptra.geocoding.google;

import com.magenta.maxoptra.dto.GeoLocationRecord;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.commons.StringUtils;
import com.magenta.maxoptra.gis.utils.*;
import com.magenta.mrs.basegeocoding.BaseGeocoding;
import com.magenta.mrs.geocoding.connector.GeoAddressStandardizer;
import geo.google.datamodel.GeoAddressAccuracy;
import geo.google.mapping.XmlToAddressFunctor;
import org.apache.commons.httpclient.util.URIUtil;
import org.apache.commons.lang.BooleanUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

import static com.magenta.maxoptra.gis.GeoStatusCode.*;
import static com.magenta.maxoptra.gis.GisParameter.*;
import static com.magenta.maxoptra.gis.MapProviderType.google;
import static com.magenta.maxoptra.gis.utils.GeoProviderHelper.getGeoDouble;
/*
* Project: Maxoptra
* Author: Kirill Sychin
* Date: 23.10.2008 12:18:37
*
* Copyright (c) 1999-2008 Magenta Corporation Ltd. All Rights Reserved.
* Magenta Technology proprietary and confidential.
* Use is subject to license terms.
*/


public class GeocoderGoogleImpl extends BaseGeocoding<GeoAddressAccuracy>
        implements IGeocoding, IGeoRouting, IReverseGeocoding {
    private static final String XML = "xml";

    private static final Map<String, String> addressFieldsMap = getFieldNameMap();
    private static final Map<String, String> reversedAddressFieldsMap = reverseMap(addressFieldsMap);

    private static final String DIRECTIONS = "Directions";
    private static final String ROUTES = "Routes";
    private static final String STEPS = "Steps";
    private static final String POINT = "Point";
    private static final String COORDINATES = "coordinates";
    private static final String END = "End";

    private static Map<String, GeoStatusCode> googleMapsErrors = new ConcurrentHashMap<String, GeoStatusCode>();

    /**
     *
     G_GEO_ABORTED_REQUEST(10101, "Status code indicating that the request was aborted due to a newer request."),
     G_GEO_SUCCESS(200, "No errors occurred; the address was successfully parsed and its geocode has been returned. (Since 2.55)"),
     G_GEO_BAD_REQUEST (400, "A directions request could not be successfully parsed. (Since 2.81)"),
     G_GEO_SERVER_ERROR (500, "A geocoding or directions request could not be successfully processed, yet the exact reason for the failure is not known. (Since 2.55)"),
     G_GEO_MISSING_QUERY (601, "The HTTP q parameter was either missing or had no value. For geocoding requests, this means that an empty address was specified as input. For directions requests, this means that no query was specified in the input. (Since 2.81)"),
     G_GEO_UNKNOWN_ADDRESS (602, "No corresponding geographic location could be found for the specified address. This may be due to the fact that the address is relatively new, or it may be incorrect. (Since 2.55)"),
     G_GEO_UNAVAILABLE_ADDRESS (603, "The geocode for the given address or the route for the given directions query cannot be returned due to legal or contractual reasons. (Since 2.55)"),
     G_GEO_UNKNOWN_DIRECTIONS (604, "The GDirections object could not compute directions between the points mentioned in the query. This is usually because there is no route available between the two points, or because we do not have data for routing in that region. (Since 2.81)"),
     G_GEO_BAD_KEY (610,  "The given key is either invalid or does not match the domain for which it was given. (Since 2.55)"),
     G_GEO_TOO_MANY_QUERIES(620, "The given key has gone over the requests limit in the 24 hour period. "),
     G_GEO_BAD_STATUS_START(600, ""),
     G_GEO_UNKOWN_STATUS (-1,  "Uknown Status"),
     //Used by simulate service.
     G_GEO_RESULT_IS_NULL (-2,  "NOT FOUND. RESULT IS NULL.");
     *
     */

    static {
        googleMapsErrors.put("10101", G_GEO_ABORTED_REQUEST);
        googleMapsErrors.put("400", G_GEO_BAD_REQUEST);
        googleMapsErrors.put("500", G_GEO_SERVER_ERROR);
        googleMapsErrors.put("601", G_GEO_MISSING_QUERY);
        googleMapsErrors.put("602", G_GEO_UNKNOWN_ADDRESS);
        googleMapsErrors.put("603", G_GEO_UNAVAILABLE_ADDRESS);
        googleMapsErrors.put("604", G_GEO_UNKNOWN_DIRECTIONS);
        googleMapsErrors.put("610", G_GEO_BAD_KEY);
        googleMapsErrors.put("620", G_GEO_TOO_MANY_QUERIES);
        googleMapsErrors.put("600", G_GEO_BAD_START_POINT);
    }

    public Map<String, GeoStatusCode> getProviderErrorMap() {
        return googleMapsErrors;
    }

    @Override
    public List<GeoAddress> doForwardGeocoding(GeoProviderSettings settings, GeoAddress address, int resultAddressLimit)
            throws GeoException {
        throw new UnsupportedOperationException("");
    }

    class GisProperties {
        private String forwardGeocodingURL;
        private String reverseGeocodingURL;
        private String navigationURL;
        private String geocodingKey;

        public String getForwardGeocodingURL() {
            return forwardGeocodingURL;
        }

        public String getReverseGeocodingURL() {
            return reverseGeocodingURL;
        }

        public String getNavigationURL() {
            return navigationURL;
        }

        public String getGeocodingKey() {
            return geocodingKey;
        }

        public GisProperties(String forwardGeocodingURL, String reverseGeocodingURL,
                             String navigationURL, String geocodingKey) {
            this.forwardGeocodingURL = forwardGeocodingURL;
            this.reverseGeocodingURL = reverseGeocodingURL;
            this.navigationURL = navigationURL;
            this.geocodingKey = geocodingKey;
        }
    }

    private GisProperties getGisProperties() throws GeoException {
        GisParameters gisParams = getProviderParams();
        String providerName = getProviderName();
        if (gisParams != null) {
            String forwardGeocodingUrl = gisParams.get(base_url.withProvider(providerName));
            if (StringUtils.isEmpty(forwardGeocodingUrl)) {
                throw new GeoException("Parameter " + base_url + " is not set for " + providerName);
            }

            String reverseGeocodingUrl = gisParams.get(reverse_geocoding_url.withProvider(providerName));
            if (StringUtils.isEmpty(reverseGeocodingUrl)) {
                throw new GeoException("Parameter " + reverse_geocoding_url + " is not set for " + providerName);
            }

            String navigationUrl = gisParams.get(routing_url.withProvider(providerName));
            if (StringUtils.isEmpty(navigationUrl)) {
                throw new GeoException("Parameter " + routing_url + " is not set for " + providerName);
            }

            String geocodingKey = gisParams.get(geocoding_key.withProvider(providerName));
            if (StringUtils.isEmpty(geocodingKey)) {
                throw new GeoException("Parameter " + geocoding_key + " is not set for " + providerName);
            }
            return new GisProperties(forwardGeocodingUrl, reverseGeocodingUrl, navigationUrl, geocodingKey);
        } else {
            throw new GeoException("Gis parameters is not set for " + providerName);
        }
    }

    public String getProviderName() {
        return google.name();
    }

    public boolean accuracyIsAccurateEnough(String accuracyThreshold, GeoAddressAccuracy accuracy) {
        GeoAddressAccuracy accuracyThresholdParsed = GeoAddressAccuracy.valueOf(accuracyThreshold);
        return accuracy.getCode() >= accuracyThresholdParsed.getCode();
    }

    public GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to, VehicleType vehicleType) throws GeoException {
        if (client == null) {
            return null;
        }

        GetRouteProfiler profiler = new GetRouteProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), from, to);

        String fromAddr = coorToStr(from);
        String toAddr = coorToStr(to);
        String mapKey = client.getProperties().get(map_key.withProvider(getProviderName()));
        if (fromAddr == null || toAddr == null || new TenThousandPerDay(mapKey).isBlockPossible()) {
            return null;
        }

        String requestResult = doRequest(from, to, mapKey);

        //Parse request result (JSON).
        try {
            JSONObject o = new JSONObject(requestResult);
            String logMsg = String.format("Request to Google has status, " +
                    "status: %s, request: from {%s} to {%s}", o.getString("Status"),
                    fromAddr, toAddr);
            if (!o.has(DIRECTIONS)) {
                logger.warn(logMsg);
                logMsg = String.format("Request to Google not success, " +
                        "status: %s, request: from {%s} to {%s}", o.getString("Status"), fromAddr, toAddr);
                logger.warn(logMsg);
                String statusCodeString = ((JSONObject) o.get("Status")).getString("code");
                Integer status = safetyParseAsInteger(statusCodeString);
                GeoStatusCode errorCode = null;
                if (status != null) {
                    errorCode = googleMapsErrors.get(statusCodeString);
                    if (errorCode == null) {
                        errorCode = G_GEO_UNKOWN_STATUS;
                    }
                }
                profiler.logError(logMsg);
                return new GeoRoute(errorCode.getCode(), -status, from, to);
            } else {
                logger.info(logMsg);
            }
            Object directions = o.get(DIRECTIONS);
            if (directions instanceof JSONObject) {
                Object distance = ((JSONObject) directions).get("Distance");
                Object duration = ((JSONObject) directions).get("Duration");
                int meters = 0;
                int seconds = 0;
                if (distance instanceof JSONObject) {
                    meters = ((JSONObject) distance).getInt("meters");
                }
                if (duration instanceof JSONObject) {
                    seconds = ((JSONObject) duration).getInt("seconds");
                }

                Double dist = UnitsConverter.convert(LineUnits.METER, LineUnits.KILOMETRE, (double) meters);
                Long dur = UnitsConverter.convert(TimeUnits.SECOND, TimeUnits.MILLI, (long) seconds);

                // logging
                String enableLoggingStr = client.getProperties().get(enableLogging.withProvider(getProviderName()));
                boolean allowLogging = BooleanUtils.toBoolean(enableLoggingStr);
                if (allowLogging) {
                    logger.info("Route information: from[%s, %s], to[%s, %s], distance=%s, duration=%s",
                            from.getLatitude(), from.getLongitude(),
                            to.getLatitude(), to.getLongitude(),
                            distance, duration);
                }

                GeoRoute geoRoute = new GeoRoute(dist, dur, from, to);
                profiler.logDuration(geoRoute);
                return geoRoute;
            }
        } catch (JSONException e) {
            profiler.logError(e.getMessage());
            logger.warn("GeocoderGoogle answer error = " + e.getMessage() + "\n from = {" + geoToStr(from, to) + "}");
            throw new GeoException(e.toString());
        }
        return null;
    }

    private String doRequest(GeographicCoordinates from, GeographicCoordinates to, String mapKey) throws GeoException {
        String requestResult;
        HttpResponse response = null;
        try {
            String request = getGeoRequestURL(from, to, mapKey);
            logger.trace("REQUEST TO GOOGLE: %s", request);
            String encodedUrl = URIUtil.encodeQuery(request);
            HttpGet get = new HttpGet(encodedUrl);
            response = client.execute(get);
            requestResult = EntityUtils.toString(response.getEntity());
        } catch (Exception e) {
            logger.warn("GeocoderGoogle answer error = " + e.getMessage() + "\n from = {" + geoToStr(from, to) + "}");
            throw new GeoException(e.toString());
        } finally {
            if (response != null) {
                //String fromAddr = coorToStr(from);
                //String toAddr = coorToStr(to);
                //logger.info("GeocoderGoogle response = " + response.getStatusLine().getReasonPhrase() +
                //        " : from {" + fromAddr + "} to {" + toAddr + "}");
            }
        }

        return requestResult;
    }

    @Override
    public List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, GeographicCoordinates from,
                                                  GeographicCoordinates to) throws GeoException {
        List<GeoLocationRecord> result = new ArrayList<GeoLocationRecord>();
        GetRoutePointsByCoordsProfiler profiler = new GetRoutePointsByCoordsProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), from, to);
        try {
            if (client != null) {
                String mapKey = client.getProperties().get(map_key.withProvider(getProviderName()));
                if (!new TenThousandPerDay(mapKey).isBlockPossible()) {
                    String requestResult = doRequest(from, to, mapKey);
                    if (StringUtils.isNotEmpty(requestResult)) {
                        result = extractRoutePointsFromJSON(from, to, requestResult);
                    }
                }
            }
            profiler.logDuration("getRoutePoints:resultCount:" + result.size());
        } catch (GeoException e) {
            profiler.logError(e.getMessage());
            throw e;
        }
        return result;
    }

    private String getGeoRequestURL(GeographicCoordinates from, GeographicCoordinates to, String mapKey) {
        String navRequest = client.getProperties().get(routing_url.withProvider(getProviderName()));
        if ( navRequest == null ) {
        	logger.error("Invalid gis property [%s] with value [%s], for provider [%s].", routing_url.getLabel(), routing_url.withProvider(getProviderName()), getProviderName());
        }
        String fromLat = getGeoDouble(from.getLatitude());
        String fromLon = getGeoDouble(from.getLongitude());
        String toLat = getGeoDouble(to.getLatitude());
        String toLon = getGeoDouble(to.getLongitude());
        String url = MessageFormat.format(navRequest, mapKey, fromLat, fromLon, toLat, toLon);
        logger.info("Making request: " + url);
        return url;
    }

    private List<GeoLocationRecord> extractRoutePointsFromJSON(GeographicCoordinates from, GeographicCoordinates to,
                                                               String response) throws GeoException {
        List<GeoLocationRecord> result = new ArrayList<GeoLocationRecord>();
        Double fromLat = from.getLatitude();
        Double fromLon = from.getLongitude();
        Double toLat = to.getLatitude();
        Double toLon = to.getLongitude();
        try {
            JSONObject o = new JSONObject(response);
            if (!o.has(DIRECTIONS)) {
                logger.warn("Request to Google not success, status: " + o.getString("Status") +
                        ", request: from {" + fromLat + ", " + fromLon + "} to {" + toLat + ", " + toLon + "}");
            } else {
                Object directions = o.get(DIRECTIONS);
                if (directions instanceof JSONObject) {
                    Object routes = ((JSONObject) directions).get(ROUTES);
                    if (routes instanceof JSONArray) {
                        Object route = ((JSONArray) routes).get(0); // берем данные только из первого маршрута
                        if (route instanceof JSONObject) {
                            Object steps = ((JSONObject) route).get(STEPS);
                            if (steps instanceof JSONArray) {
                                JSONArray stepsArray = (JSONArray) steps;
                                for (int stIndex = 0; stIndex < stepsArray.length(); stIndex++) {
                                    Object points = stepsArray.get(stIndex);
                                    if (points instanceof JSONObject) {
                                        JSONObject pointsObject = (JSONObject) points;
                                        Object point = pointsObject.get(POINT);
                                        if (point instanceof JSONObject) {
                                            Object coordinates = ((JSONObject) point).get(COORDINATES);
                                            createLocationByJSONCoordinates(result, coordinates);
                                        }
                                    }
                                }
                            }

                            // завершение маршрута
                            Object end = ((JSONObject) route).get(END);
                            if (end instanceof JSONObject) {
                                JSONObject endObj = (JSONObject) end;
                                Object endCoordinates = endObj.get(COORDINATES);
                                createLocationByJSONCoordinates(result, endCoordinates);
                            }
                        }
                    }
                }
            }
        } catch (JSONException e) {
            logger.warn("GeocoderGoogle answer error = " + e.getMessage() + "\n from = {" + geoToStr(from, to) + "}");
            throw new GeoException(e.toString());
        } catch (Exception e) {
            logger.warn("GeocoderGoogle answer error = " + e.getMessage() + "\n from = {" + geoToStr(from, to) + "}");
            throw new GeoException(e.toString());
        }
        return result;
    }

    private void createLocationByJSONCoordinates(List<GeoLocationRecord> result, Object coordinates) throws JSONException {
        if (coordinates instanceof JSONArray) {
            JSONArray coordArray = (JSONArray) coordinates;
            Double lon = (Double) coordArray.get(0);
            Double lat = (Double) coordArray.get(1);
            GeoLocationRecord location = new GeoLocationRecord(lon, lat);
            result.add(location);
        }
    }

    private String geoToStr(GeographicCoordinates from, GeographicCoordinates to) {
        String fromAddr = String.format("%s%%2C%s",
                Double.toString(from.getLatitude()),
                Double.toString(from.getLongitude()));
        String toAddr = String.format("%s%%2C%s", Double.toString(to.getLatitude()), Double.toString(to.getLongitude()));
        return fromAddr + ", " + toAddr;
    }

    private String coorToStr(GeographicCoordinates geoCoor) {
        return String.format("%s%%2C%s", Double.toString(geoCoor.getLatitude()), Double.toString(geoCoor.getLongitude()));
    }

    protected List<GeoAddress> doFindAddressByKeyword(GeoProviderSettings settings, String keyword, int i,
                                                      List<GeoAddressAccuracy> accuracies) throws GeoException {
        GisProperties gisParams = getGisProperties();
        String apiKey = gisParams.getGeocodingKey();
        if (apiKey == null) {
            return new ArrayList<GeoAddress>();
        }
        try {
            keyword = URLEncoder.encode(keyword, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new GeoException(e.getMessage());
        }
        String urlString = MessageFormat.format(gisParams.getForwardGeocodingURL(), keyword, XML, apiKey);
        return getAddressesForUrl(urlString, i, accuracies);
    }

    private List<GeoAddress> getAddressesForUrl(String urlString, int maxCount, List<GeoAddressAccuracy> accuracies)
            throws GeoException {
        List<GeoAddress> list = new ArrayList<GeoAddress>();
        GeoAddressStandardizer standardizer;
        standardizer = new GeoAddressStandardizer();
        standardizer.setClient(client);
        standardizer.setCanSleepDueToGeocodingLimit(true);
        int addrCount = 0;
        List<geo.google.datamodel.GeoAddress> addressList =
                standardizer.standardize(urlString, XmlToAddressFunctor.getInstance());
        for (geo.google.datamodel.GeoAddress address : addressList) {
            final GeoAddress geoAddress = parseGeoAddress(address);
            if (accuracies != null) {
                accuracies.add(address.getAccuracy());
            }
            list.add(geoAddress);
            addrCount++;
            if (addrCount == maxCount) {
                break;
            }
        }
        return list;
    }

    private <T> String getValueFromGeoField(final T geoField) {
        if (geoField instanceof List) {
            return getValueFromGeoArray((List) geoField);
        } else {
            return geoField.toString();
        }
    }

    private <T> String getValueFromGeoArray(final List<T> geoArray) {
        if (geoArray.size() == 0) {
            return null;
        }
        Class clazz = geoArray.get(0).getClass();
        Collection results;
        if (clazz.equals(String.class)) {
            results = geoArray;
        } else {
            Method getContentMethod = null;
            for (Method method : clazz.getMethods()) {
                if (method.getName().equals("getContent")) {
                    getContentMethod = method;
                    break;
                }
            }
            if (getContentMethod == null) {
                return null;
            }
            results = new ArrayList<String>();
            try {
                for (T item : geoArray) {
                    results.add(getContentMethod.invoke(item).toString());
                }
            } catch (IllegalAccessException e) {
                throw new IllegalStateException(e);
            } catch (InvocationTargetException e) {
                throw new IllegalStateException(e);
            }
        }
        return StringUtils.join(results, ", ");
    }


    private static Map<String, String> reverseMap(Map<String, String> fieldnameMap) {
        //STATE and COUNTY clash here but its OK
        Map<String, String> fields = new HashMap<String, String>();
        for (String fieldKey : fieldnameMap.keySet()) {
            fields.put(fieldnameMap.get(fieldKey), fieldKey);
        }
        return fields;
    }

    private GeoAddress parseGeoAddress(geo.google.datamodel.GeoAddress address) {
        Map<String, Object> addrPartsHeap = new HashMap<String, Object>();
        parseAddrDetails(addrPartsHeap, address.getAddressDetails());

        if (!addrPartsHeap.containsKey("localityName") && addrPartsHeap.containsKey("dependentLocalityName")) {
            addrPartsHeap.put("localityName", addrPartsHeap.get("dependentLocalityName"));
        }
        if (addrPartsHeap.size() <= 2 && addrPartsHeap.get("addressLine") == null) {
            addrPartsHeap.put("addressLine", address.getAddressLine());
        }

        final GeoAddress geoAddress = new GeoAddress();

        geoAddress.setLatitude(address.getCoordinate().getLatitude());
        geoAddress.setLongitude(address.getCoordinate().getLongitude());
        for (AddressSegment addressSegment : AddressSegment.values()) {
            Object valueObj = addrPartsHeap.get(addressFieldsMap.get(addressSegment.name()));
            if (valueObj == null || !(valueObj instanceof String)) {
                continue;
            }
            setAddrSegment(geoAddress, addressSegment, (String) valueObj);
        }

        return geoAddress;
    }

    private void setAddrSegment(GeoAddress geoAddress, AddressSegment addressSegment, String value) {
        switch (addressSegment) {
            case INFO:
                geoAddress.setInfo(value);
                break;
            case STREET:
                geoAddress.setStreet(value);
                break;
            case CITY:
                geoAddress.setCity(value);
                break;
            case COUNTRY:
                geoAddress.setCountry(value);
                break;
            case DISTRICT:
                geoAddress.setDistrict(value);
                break;
            case STATE:
                geoAddress.setState(value);
                break;
            case COUNTY:
                geoAddress.setCounty(value);
                break;
            case ZIP:
                geoAddress.setZip(value);
                break;
            default:
                throw new IllegalStateException("unknown address part");
        }
    }

    private static Map<String, String> getFieldNameMap() {
        Map<String, String> fieldnameMap = new HashMap<String, String>();
        fieldnameMap.put(AddressSegment.INFO.toString(), "addressLine");
        //Free format address representation. An address can have more than one line.
        // The order of the AddressLine elements must be preserved
        fieldnameMap.put(AddressSegment.STREET.toString(), "thoroughfareName");
        //Specification of the name of a Thoroughfare (also dependant street name): street name, canal name, etc.
        fieldnameMap.put(AddressSegment.CITY.toString(), "localityName");

        //Locality is one level lower than adminisstrative area. Eg.: cities, reservations and any other built-up areas.
        fieldnameMap.put("dependentLocality", "dependentLocalityName");
        //todo: may be its better to omit DependentLocality
        //dependent localities are Districts within cities/towns, locality divisions, postal
        //divisions of cities, suburbs, etc. DependentLocality is a recursive element, but no nesting
        //deeper than two exists (Locality-DependentLocality-DependentLocality)
        fieldnameMap.put(AddressSegment.STATE.toString(), "administrativeAreaName");
        fieldnameMap.put(AddressSegment.COUNTY.toString(), "administrativeAreaName");
        //Name of the administrative area. eg. MI in USA, NSW in Australia
        fieldnameMap.put(AddressSegment.DISTRICT.toString(), "subAdministrativeAreaName");

        fieldnameMap.put(AddressSegment.ZIP.toString(), "postalCodeNumber");
        fieldnameMap.put(AddressSegment.COUNTRY.toString(), "countryNameCode");
        return fieldnameMap;
    }

    private void parseAddrDetails(Map<String, Object> addrPartsHeap, Object addrPart) {
        if (addrPart == null) {
            return;
        }

        final BeanInfo classInfo;
        try {
            classInfo = Introspector.getBeanInfo(addrPart.getClass());
        } catch (IntrospectionException e) {
            throw new IllegalStateException(e);
        }
        try {
            for (final PropertyDescriptor pd : classInfo.getPropertyDescriptors()) {
                if (!pd.getName().equals("interceptFieldCallback")) {
                    final Method getMethod = pd.getReadMethod();
                    if (getMethod != null) {
                        final String name = pd.getName();
                        if (name.equals("class")) { //Exclude class property
                            continue;
                        }

                        Object value = getMethod.invoke(addrPart);
                        if (reversedAddressFieldsMap.containsKey(name)) {
                            addrPartsHeap.put(name, getValueFromGeoField(value));
                        } else {
                            parseAddrDetails(addrPartsHeap, value);
                        }
                    }
                }
            }
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        } catch (InvocationTargetException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public GeoAddress doReverseGeocoding(GeographicCoordinates geoCoords) throws GeoException {
        GisParameters properties = client.getProperties();
        String providerName = getProviderName();
        String apiKey = properties.get(map_key.withProvider(providerName));
        if (apiKey == null) {
            return null;
        }
        String revRequest = properties.get(reverse_geocoding_url.withProvider(providerName));
        String urlString = MessageFormat.format(revRequest, XML,
                Double.toString(geoCoords.getLatitude()), Double.toString(geoCoords.getLongitude()), apiKey);

        // logging
        String enableLoggingStr = client.getProperties().get(enableLogging.withProvider(getProviderName()));
        boolean allowLogging = BooleanUtils.toBoolean(enableLoggingStr);
        if (allowLogging) {
            logger.info(urlString);
        }

        List<GeoAddress> result = getAddressesForUrl(urlString, 1, null);
        return result.size() != 0 ? result.get(0) : null;
    }

    private static final Pattern DECIMAL_INTEGER = Pattern.compile("-?\\d+");

    public static Integer safetyParseAsInteger(String text) {
        if (text != null && (DECIMAL_INTEGER.matcher(text.trim())).matches()) {
            return Integer.valueOf(text.trim());
        }
        return null;
    }

    @Override
    public List<GeoAddress> doReverseGeocodeFull(List<GeographicCoordinates> geoCoordinates) throws GeoException {
        throw new UnsupportedOperationException("doReverseGeocodeFull is not supported for Google");
    }

    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates,VehicleType vehicleType) throws GeoException {
        throw new UnsupportedOperationException("getRoute by list of coordinates is not supported for Google");
    }

    @Override
    public List<GeoRoute> getRoute(final GeographicCoordinates from, final List<? extends GeographicCoordinates> to, VehicleType vehicleType, int timeout) throws GeoException {
        throw new UnsupportedOperationException("getRoute from coordinate to list of coordinates is not supported for Google");
    }

    public synchronized List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, List<GeographicCoordinates> coordinates) throws GeoException {
        throw new UnsupportedOperationException("getRoutePoints by list of coordinates is not supported for Google");
    }

    public GeoAddress findCountry(String country, String keyword) throws GeoException {
        throw new UnsupportedOperationException("IGeocoding.findCountry is not implemented for Google");
    }

    public enum AddressSegment {
        INFO, STREET, CITY, COUNTRY, DISTRICT, STATE, COUNTY, ZIP
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from, List<? extends GeographicCoordinates> to, VehicleType vehicleType, int timeout) throws GeoException {
        throw new UnsupportedOperationException("getRoutes from coordinate to list of coordinates is not supported for Google");
    }

    @Override
    public List<GeoRoute> getCoordinateToCoordinateRoutes(List<GeographicCoordinates> coordinates) throws ServerUnavailableException {
        throw new UnsupportedOperationException("getCoordinateToCoordinateRoutes from coordinate to list of coordinates is not supported for " + getProviderName());
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from, List<? extends GeographicCoordinates> to, int timeout) throws GeoException, ServerUnavailableException {
        return getRoutes(from, to, timeout);
    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from, List<? extends GeographicCoordinates> to, int timeout) throws GeoException {
        return getRoute(from, to, null, timeout);
    }

    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates) throws GeoException {
        return getRoute(coordinates, null);
    }

    @Override
    public synchronized GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to) throws GeoException, ServerUnavailableException {
        return getRoute(from, to, null);
    }

}
